import KeyCodes from "../../../utils/key-codes";
import { arrayIncludes, from as arrayFrom } from "../../../utils/array";
import { closest, isElement } from "../../../utils/dom";
import { props as tbodyProps, NlyTbody } from "../tbody";
import filterEvent from "./filter-event";
import textSelectionActive from "./text-selection-active";
import tbodyRowMixin from "./mixin-tbody-row";

const props = {
  ...tbodyProps,
  tbodyClass: {
    type: [String, Array, Object]
    // default: undefined
  }
};

export default {
  mixins: [tbodyRowMixin],
  props,
  methods: {
    // Helper methods
    getTbodyTrs() {
      // Returns all the item TR elements (excludes detail and spacer rows)
      // `this.$refs.itemRows` is an array of item TR components/elements
      // Rows should all be nly-TR components, but we map to TR elements
      // Also note that `this.$refs.itemRows` may not always be in document order
      const refs = this.$refs || {};
      const tbody = refs.tbody ? refs.tbody.$el || refs.tbody : null;
      const trs = (refs.itemRows || []).map(tr => tr.$el || tr);
      return tbody &&
        tbody.children &&
        tbody.children.length > 0 &&
        trs &&
        trs.length > 0
        ? arrayFrom(tbody.children).filter(tr => arrayIncludes(trs, tr))
        : [];
    },
    getTbodyTrIndex(el) {
      // Returns index of a particular TBODY item TR
      // We set `true` on closest to include self in result
      /* istanbul ignore next: should not normally happen */
      if (!isElement(el)) {
        return -1;
      }
      const tr = el.tagName === "TR" ? el : closest("tr", el, true);
      return tr ? this.getTbodyTrs().indexOf(tr) : -1;
    },
    emitTbodyRowEvent(type, evt) {
      // Emits a row event, with the item object, row index and original event
      if (type && this.hasListener(type) && evt && evt.target) {
        const rowIndex = this.getTbodyTrIndex(evt.target);
        if (rowIndex > -1) {
          // The array of TRs correlate to the `computedItems` array
          const item = this.computedItems[rowIndex];
          this.$emit(type, item, rowIndex, evt);
        }
      }
    },
    tbodyRowEvtStopped(evt) {
      return this.stopIfBusy && this.stopIfBusy(evt);
    },
    // Delegated row event handlers
    onTbodyRowKeydown(evt) {
      // Keyboard navigation and row click emulation
      const target = evt.target;
      if (
        this.tbodyRowEvtStopped(evt) ||
        target.tagName !== "TR" ||
        target !== document.activeElement ||
        target.tabIndex !== 0
      ) {
        // Early exit if not an item row TR
        return;
      }
      const keyCode = evt.keyCode;
      if (arrayIncludes([KeyCodes.ENTER, KeyCodes.SPACE], keyCode)) {
        // Emulated click for keyboard users, transfer to click handler
        evt.stopPropagation();
        evt.preventDefault();
        this.onTBodyRowClicked(evt);
      } else if (
        arrayIncludes(
          [KeyCodes.UP, KeyCodes.DOWN, KeyCodes.HOME, KeyCodes.END],
          keyCode
        )
      ) {
        // Keyboard navigation
        const rowIndex = this.getTbodyTrIndex(target);
        if (rowIndex > -1) {
          evt.stopPropagation();
          evt.preventDefault();
          const trs = this.getTbodyTrs();
          const shift = evt.shiftKey;
          if (keyCode === KeyCodes.HOME || (shift && keyCode === KeyCodes.UP)) {
            // Focus first row
            trs[0].focus();
          } else if (
            keyCode === KeyCodes.END ||
            (shift && keyCode === KeyCodes.DOWN)
          ) {
            // Focus last row
            trs[trs.length - 1].focus();
          } else if (keyCode === KeyCodes.UP && rowIndex > 0) {
            // Focus previous row
            trs[rowIndex - 1].focus();
          } else if (keyCode === KeyCodes.DOWN && rowIndex < trs.length - 1) {
            // Focus next row
            trs[rowIndex + 1].focus();
          }
        }
      }
    },
    onTBodyRowClicked(evt) {
      if (this.tbodyRowEvtStopped(evt)) {
        // If table is busy, then don't propagate
        return;
      } else if (filterEvent(evt) || textSelectionActive(this.$el)) {
        // Clicked on a non-disabled control so ignore
        // Or user is selecting text, so ignore
        return;
      }
      this.emitTbodyRowEvent("row-clicked", evt);
    },
    onTbodyRowMiddleMouseRowClicked(evt) {
      if (!this.tbodyRowEvtStopped(evt) && evt.which === 2) {
        this.emitTbodyRowEvent("row-middle-clicked", evt);
      }
    },
    onTbodyRowContextmenu(evt) {
      if (!this.tbodyRowEvtStopped(evt)) {
        this.emitTbodyRowEvent("row-contextmenu", evt);
      }
    },
    onTbodyRowDblClicked(evt) {
      if (!this.tbodyRowEvtStopped(evt) && !filterEvent(evt)) {
        this.emitTbodyRowEvent("row-dblclicked", evt);
      }
    },
    // Note: Row hover handlers are handled by the tbody-row mixin
    // As mouseenter/mouseleave events do not bubble
    //
    // Render Helper
    renderTbody() {
      // Render the tbody element and children
      const items = this.computedItems;
      // Shortcut to `createElement` (could use `this._c()` instead)
      const h = this.$createElement;
      const hasRowClickHandler =
        this.hasListener("row-clicked") || this.hasSelectableRowClick;

      // Prepare the tbody rows
      const $rows = [];

      // Add the item data rows or the busy slot
      const $busy = this.renderBusy ? this.renderBusy() : null;
      if ($busy) {
        // If table is busy and a busy slot, then return only the busy "row" indicator
        $rows.push($busy);
      } else {
        // Table isn't busy, or we don't have a busy slot

        // Create a slot cache for improved performance when looking up cell slot names
        // Values will be keyed by the field's `key` and will store the slot's name
        // Slots could be dynamic (i.e. `v-if`), so we must compute on each render
        // Used by tbody-row mixin render helper
        const cache = {};
        const defaultSlotName = this.hasNormalizedSlot("cell()")
          ? "cell()"
          : null;
        this.computedFields.forEach(field => {
          const key = field.key;
          const fullName = `cell(${key})`;
          const lowerName = `cell(${key.toLowerCase()})`;
          cache[key] = this.hasNormalizedSlot(fullName)
            ? fullName
            : this.hasNormalizedSlot(lowerName)
            ? lowerName
            : defaultSlotName;
        });
        // Created as a non-reactive property so to not trigger component updates
        // Must be a fresh object each render
        this.$_bodyFieldSlotNameCache = cache;

        // Add static top row slot (hidden in visibly stacked mode
        // as we can't control `data-label` attr)
        $rows.push(this.renderTopRow ? this.renderTopRow() : h());

        // Render the rows
        items.forEach((item, rowIndex) => {
          // Render the individual item row (rows if details slot)
          $rows.push(this.renderTbodyRow(item, rowIndex));
        });

        // Empty items / empty filtered row slot (only shows if `items.length < 1`)
        $rows.push(this.renderEmpty ? this.renderEmpty() : h());

        // Static bottom row slot (hidden in visibly stacked mode
        // as we can't control `data-label` attr)
        $rows.push(this.renderBottomRow ? this.renderBottomRow() : h());
      }

      // Note: these events will only emit if a listener is registered
      const handlers = {
        auxclick: this.onTbodyRowMiddleMouseRowClicked,
        // TODO:
        //   Perhaps we do want to automatically prevent the
        //   default context menu from showing if there is a
        //   `row-contextmenu` listener registered
        contextmenu: this.onTbodyRowContextmenu,
        // The following event(s) is not considered A11Y friendly
        dblclick: this.onTbodyRowDblClicked
        // Hover events (`mouseenter`/`mouseleave`) are handled by `tbody-row` mixin
      };
      // Add in click/keydown listeners if needed
      if (hasRowClickHandler) {
        handlers.click = this.onTBodyRowClicked;
        handlers.keydown = this.onTbodyRowKeydown;
      }

      // Assemble rows into the tbody
      const $tbody = h(
        NlyTbody,
        {
          ref: "tbody",
          class: this.tbodyClass || null,
          props: {
            tbodyTransitionProps: this.tbodyTransitionProps,
            tbodyTransitionHandlers: this.tbodyTransitionHandlers
          },
          // BTbody transfers all native event listeners to the root element
          // TODO: Only set the handlers if the table is not busy
          on: handlers
        },
        $rows
      );

      // Return the assembled tbody
      return $tbody;
    }
  }
};
